Category 的加载处理过程
在这篇博客 iOS 程序 main 函数之前发生了什么 有中提到,_objc_init 这个函数是 runtime 系统的初始化函数,于是我们可以直接从 _objc_init 这个函数开始进行分析, Category 加载过程中的函数调用顺序如下:
1 | void _objc_init(void); |
| 文件名 | 方法 |
|---|---|
| objc-os.mm | _objc_init |
| objc-os.mm | map_images |
| objc-os.mm | map_images_nolock |
| objc-runtime-new.mm | _read_images |
| objc-runtime-new.mm | addUnattachedCategoryForClass |
| objc-runtime-new.mm | remethodizeClass |
| objc-runtime-new.mm | attachCategories |
| objc-runtime-new.mm | attachLists |
_read_images 函数处理当前镜像文件的头部信息,具体步骤:
- 获取镜像文件中的类列表,遍历列表进行类读取(调用
readClass) - 遍历注册所有的
selector名字(调用__sel_registerName) - 遍历读取协议列表(调用
_getObjc2ProtocolList,readProtocol) - 遍历读取分类列表(调用
_getObjc2CategoryList,addUnattachedCategoryForClass,remethodizeClass) - 遍历实例化运行时类结构(调用
realizeClass)
从中可以找到与分类相关的代码:
1 | // Discover categories. |
在上面的代码中,首先通过函数 _getObjc2CategoryList 获取 category 的列表 catlist,然后遍历 catlist,获取 category 的 Class,根据 Class(类对象和元类对象) 的实例方法、协议、属性,来判断调用addUnattachedCategoryForClass 函数,并进一步判断是否调用 remethodizeClass 函数。
1 | static void addUnattachedCategoryForClass(category_t *cat, Class cls, |
在 addUnattachedCategoryForClass 函数中通过 unattachedCategories() 函数生成一个单例 MapTable 对象 cats,从该对象中获取 category 的 list 指针,判断该 list 指针是否为空,分配相应内存空间,最后将 category 的数据插入到单例 MapTable 中。
在 remethodizeClass 函数中将通过 attachCategories 函数,把分类信息附加到相应的类中。attachCategories 函数会将类别中的方法列表,属性和协议列表分别都加入本类中,并假定了类别列表加载的顺序是根据类别文件的加载顺序。
1 | static void |
其中的加载函数为 attachLists,其关键实现为:
1 | array()->count = newCount; |
在 attachLists 方法主要关注两个变量 array()->lists 和 addedLists
- array()->lists: 类对象原来的方法列表,属性列表,协议列表
- addedLists:传入所有分类的方法列表,属性列表,协议列表
上面代码的作用就是通过 memmove 将原来的类找那个的方法、属性、协议列表分别进行后移,然后通过 memcpy 将传入的方法、属性、协议列表填充到开始的位置。
这里总结一下上面的过程:
1、通过 Runtime 加载某个类的所有 Category 数据
2、把所有 Category 的方法、属性、协议数据,合并到一个大数组中,后面参与编译的 Category 数据,会在数组的前面
3、将合并后的分类数据(方法、属性、协议),插入到类原来数据的前面
拓展
load 源码分析
通过 objc4 中的源码进行分析, load 加载过程中的函数调用顺序如下:
1 | void _objc_init(void); |
在 load_images 函数中核心逻辑是调用 prepare_load_methods 与 call_load_methods。
prepare_load_methods 函数的作用就是提前准备好满足 +load 方法调用条件的类和分类,以供接下来的调用。 然后在这个类中调用了schedule_class_load(Class cls)方法,并且在入参时对父类递归的调用了,确保父类优先的顺序。
call_load_methods 函数中循环调用所有类的 +load 方法。注意,这里是(调用分类的 +load 方法也是如此)直接使用函数内存地址的方式 (*load_method)(cls, SEL_load); 对 +load 方法进行调用的,而不是使用发送消息 objc_msgSend 的方式。
- 分析
call_load_methods源码
1 | void call_load_methods(void) |
从中可以看出
1、通过 do-while 循环加载类的 load 方法(call_class_loads 是实现 +load 方法的核心函数);
2、先调用完所有类的 load 方法,再调用分类的 load 方法。
- 分析
call_class_loads源码
1 | static void call_class_loads(void) |
这个函数的作用就是真正负责调用类的 +load 方法了。它从全局变量 loadable_classes 中取出所有可供调用的类,并进行清零操作,其中 loadable_classes 指向用于保存类信息的内存的首地址,loadable_classes_allocated标识已分配的内存空间大小,loadable_classes_used 则标识已使用的内存空间大小。然后,循环调用所有类的 +load 方法。注意,这里是(调用分类的 +load 方法也是如此)直接使用函数内存地址的方式 (*load_method)(cls, SEL_load); 对 +load 方法进行调用的,而不是使用发送消息 objc_msgSend 的方式。
+ (void)load 小总结
+load方法会在 runtime 加载类、分类时调用- 每个类、分类的+load,在程序运行过程中只调用一次
- 调用顺序:父类 -> 子类 -> 父类的 category -> 子类的 category
1 | 2018-12-03 15:33:36.915480+0800 CALayerDemo[20979:460542] Foo +[Foo load] |
initialize 源码分析
通过 objc4 中的源码进行分析, initialize 加载过程中的函数调用顺序如下:
1 | Method class_getInstanceMethod(Class cls, SEL sel); |
+ (void)initialize 小总结
+initialize方法会在类第一次接收到消息时调用- 先调用父类的 +initialize,再调用子类的 +initialize
- 先初始化父类,再初始化子类,每个类只会初始化1次
1 | 2018-12-03 15:33:36.916315+0800 CALayerDemo[20979:460542] Foo(Test) +[Foo(Test) initialize] |
load 与 initialize对比
| 条件 | +load | +initialize |
|---|---|---|
| 关键方法 | (*load_method)(cls, SEL_load) |
objc_msgSend |
| 调用时机 | 被添加到 runtime 时 | 收到第一条消息前,可能永远不调用 |
| 调用顺序 | 父类 -> 子类 -> 父类分类 -> 子类分类 | 父类 -> 子类或(父类分类 -> 子类分类) |
| 调用次数 | 1次 | 多次 |
| 是否需要显式调用父类实现 | 否 | 否 |
| 是否沿用父类的实现 | 否 | 是 |
| 分类中的实现 | 类和分类都执行 | 覆盖类中的方法,只执行分类的实现 |